package com.example.jpa.crud.search.expression;

import com.example.jpa.crud.exception.ExpressionException;
import com.example.jpa.crud.search.Operator;
import com.example.jpa.crud.search.OperatorSupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;

import java.util.*;

public class SearchExpressionResolver {

    private static final Logger log = LoggerFactory.getLogger(SearchExpressionResolver.class);

    public static ASTNode resolve(String text) {
        if (!StringUtils.hasText(text)) {
            return null;
        }
        final List<Token> tokens = getTokens(text);
        final TokenReader reader = new TokenReader(tokens);
        ASTNode ast;
        try {
            ast = tryResolveBooleanExpressionNode(reader);
        } catch (ExpressionException e) {
            log.error("'{}' cannot be parsed correctly, {}", text, e.getMessage());
            throw new ExpressionException("'" + text + "' cannot be parsed correctly, " + e.getMessage());
        }
        return ast;
    }

    private enum TokenType {
        LEFT_PARENTHESIS,
        RIGHT_PARENTHESIS,
        IDENTIFIER,
        OPERATOR,
        PARAMETER_NAME
    }

    private static class Token {
        TokenType type;
        String value;

        Token(TokenType type, String value) {
            this.type = type;
            this.value = value;
        }

        static Token LEFT_PARENTHESIS = new Token(TokenType.LEFT_PARENTHESIS, "(");
        static Token RIGHT_PARENTHESIS = new Token(TokenType.RIGHT_PARENTHESIS, "(");
        static Map<String, Token> OPERATOR_TOKEN_MAP = new HashMap<>();
        static Map<Token, Operator> TOKEN_OPERATOR_MAP = new HashMap<>();

        static {
            for (Operator operator : Operator.values()) {
                final Token token = new Token(TokenType.OPERATOR, operator.name());
                OPERATOR_TOKEN_MAP.put(operator.name(), token);
                TOKEN_OPERATOR_MAP.put(token, operator);
            }
        }
    }


    private static class TokenReader {
        private final List<Token> tokens;
        private int i;

        TokenReader(List<Token> tokens) {
            this.tokens = tokens;
            this.i = 0;
        }

        Token peek() {
            if (this.i >= 0 && this.i < tokens.size()) {
                return tokens.get(this.i);
            } else {
                return null;
            }
        }

        void forward() {
            if (this.i >= 0 && this.i < tokens.size()) {
                this.i++;
            }
        }

        void backward() {
            if (this.i > 0) {
                this.i--;
            }
        }
    }

    private static Token buildToken(int[] codePoints, int startIndex, int endIndex) {
        if (codePoints[startIndex] == ':') {
            if (endIndex - startIndex > 1) {
                final String parameterName = new String(codePoints, startIndex + 1, endIndex - startIndex - 1);
                return new Token(TokenType.PARAMETER_NAME, parameterName);
            } else {
                return null;
            }
        } else {
            final String identifier = new String(codePoints, startIndex, endIndex - startIndex);
            final String identifierUpperCase = identifier.toUpperCase();
            if (Token.OPERATOR_TOKEN_MAP.containsKey(identifierUpperCase)) {
                return Token.OPERATOR_TOKEN_MAP.get(identifierUpperCase);
            } else {
                return new Token(TokenType.IDENTIFIER, identifier);
            }
        }
    }

    private static List<Token> getTokens(String text) {
        final int textLength = text.length();
        final int[] codePoints = new int[textLength];
        int length = 0;
        int i = 0;
        int codePoint;
        while (i < textLength) {
            codePoint = text.codePointAt(i);
            codePoints[length++] = codePoint;
            i += Character.charCount(codePoint);
        }
        final List<Token> tokens = new ArrayList<>();
        Token token;
        int startIndex = -1;
        i = 0;
        while (i < length) {
            if (Character.isWhitespace(codePoints[i])) {
                if (startIndex > -1) {
                    token = buildToken(codePoints, startIndex, i);
                    if (token != null) {
                        tokens.add(token);
                    }
                    startIndex = -1;
                }
                i++;
            } else {
                int c = codePoints[i];
                if (c == '(') {
                    if (startIndex > -1) {
                        token = buildToken(codePoints, startIndex, i);
                        if (token != null) {
                            tokens.add(token);
                        }
                        startIndex = -1;
                    }
                    tokens.add(Token.LEFT_PARENTHESIS);
                    i++;
                } else if (c == ')') {
                    if (startIndex > -1) {
                        token = buildToken(codePoints, startIndex, i);
                        if (token != null) {
                            tokens.add(token);
                        }
                        startIndex = -1;
                    }
                    tokens.add(Token.RIGHT_PARENTHESIS);
                    i++;
                } else {
                    if (startIndex == -1) {
                        startIndex = i;
                    }
                    i++;
                }
            }
        }
        if (startIndex > -1) {
            token = buildToken(codePoints, startIndex, length);
            if (token != null) {
                tokens.add(token);
            }
        }
        return tokens;
    }

    private static ASTNode tryResolveBinaryExpressionNode(TokenReader reader) throws ExpressionException {
        Token token = reader.peek();
        if (token == null) {
            return null;
        }
        ASTNode node;
        if (token.type == TokenType.LEFT_PARENTHESIS) {
            reader.forward();
            final ASTNode binaryExpression = tryResolveBinaryExpressionNode(reader);
            if (binaryExpression == null) {
                throw new ExpressionException("expect BinaryExpression");
            }
            token = reader.peek();
            if (token == null || token.type != TokenType.RIGHT_PARENTHESIS) {
                throw new ExpressionException("expect ')'");
            }
            reader.forward();
            node = binaryExpression;
        } else {
            if (token.type != TokenType.IDENTIFIER) {
                throw new ExpressionException("expect IDENTIFIER");
            }
            final ASTNode identifier = new ASTNode();
            identifier.setType(ASTNodeType.IDENTIFIER);
            identifier.setValue(token.value);
            reader.forward();
            token = reader.peek();
            if (token == null || token.type != TokenType.OPERATOR) {
                throw new ExpressionException("expect OPERATOR: " + OperatorSupport.binaryOperators);
            }
            Operator operator = Token.TOKEN_OPERATOR_MAP.get(token);
            if (operator == null || operator == Operator.AND || operator == Operator.OR) {
                throw new ExpressionException("expect OPERATOR: " + OperatorSupport.binaryOperators);
            }
            reader.forward();
            token = reader.peek();
            if (token == null || token.type != TokenType.PARAMETER_NAME) {
                throw new ExpressionException("expect PARAMETER_NAME");
            }
            final ASTNode parameterName = new ASTNode();
            parameterName.setType(ASTNodeType.PARAMETER_NAME);
            parameterName.setValue(token.value);
            reader.forward();
            final ASTNode binaryExpression = new ASTNode();
            binaryExpression.setType(ASTNodeType.BINARY_EXPRESSION);
            binaryExpression.setLeft(identifier);
            binaryExpression.setOperator(operator);
            binaryExpression.setRight(parameterName);
            node = binaryExpression;
        }
        return node;
    }

    private static ASTNode tryResolveBooleanExpressionNode(TokenReader reader) throws ExpressionException {
        Token token = reader.peek();
        if (token == null) {
            return null;
        }
        ASTNode node;
        if (token.type == TokenType.LEFT_PARENTHESIS) {
            reader.forward();
            final ASTNode booleanExpression = tryResolveBooleanExpressionNode(reader);
            if (booleanExpression == null) {
                throw new ExpressionException("expect BooleanExpression");
            }
            token = reader.peek();
            if (token == null || token.type != TokenType.RIGHT_PARENTHESIS) {
                throw new ExpressionException("expect ')'");
            }
            reader.forward();
            node = booleanExpression;
        } else {
            final ASTNode binaryExpression = tryResolveBinaryExpressionNode(reader);
            if (binaryExpression == null) {
                throw new ExpressionException("expect BinaryExpression");
            }
            // BinaryExpression is BooleanExpression
            node = binaryExpression;
        }
        token = reader.peek();
        final Operator operator = Token.TOKEN_OPERATOR_MAP.get(token);
        if (operator == Operator.AND || operator == Operator.OR) {
            reader.forward();
            final ASTNode booleanExpression = tryResolveBooleanExpressionNode(reader);
            if (booleanExpression == null) {
                throw new ExpressionException("expect BooleanExpression");
            }
            final ASTNode newNode = new ASTNode();
            newNode.setType(ASTNodeType.BOOLEAN_EXPRESSION);
            newNode.setLeft(node);
            newNode.setOperator(operator);
            newNode.setRight(booleanExpression);
            node = newNode;
        }
        return node;
    }

}
